package com.redxiii.spike.services.xmpp;

import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.store.MemoryStoreEvictionPolicy;

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster.SubscriptionMode;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Presence.Mode;
import org.jivesoftware.smack.packet.Presence.Type;
import org.jivesoftware.smackx.filetransfer.FileTransferListener;
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
import org.jivesoftware.smackx.filetransfer.FileTransferNegotiator;
import org.jivesoftware.smackx.filetransfer.FileTransferRequest;
import org.jivesoftware.smackx.filetransfer.IncomingFileTransfer;
import org.jivesoftware.smackx.filetransfer.OutgoingFileTransfer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.redxiii.spike.api.message.ImMessage;
import com.redxiii.spike.api.message.ImStatus;
import com.redxiii.spike.api.message.Message;
import com.redxiii.spike.util.communication.BaseCommunicationService;
import com.redxiii.spike.util.core.StorageSystem;


/**
 * @author Daniel Filgueiras
 * 04/05/2011
 */
public class XmppService extends BaseCommunicationService implements PacketListener, FileTransferListener {

	private static final Logger logger = LoggerFactory.getLogger(XmppService.class);
	
	private int		watchDog = 5;
	
	private String login;
	private String password;
	
	private XMPPConnection connection;
	private FileTransferManager manager;
	
	private Cache presenceCache;
	
	private Set<String> blockedUsers = new HashSet<String>();
	private Map<String,String> proxyUsers = new HashMap<String,String>();
	
	public XmppService() {
		
		presenceCache = new Cache(
				new CacheConfiguration("xmppPresence", 50)
					.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
					.overflowToDisk(false)
					.eternal(false)
					.timeToLiveSeconds(3600)	//enables cached object to be kept in as long as it is requested in periods shorter than timeToIdleSeconds
					.timeToIdleSeconds(3600));	//will make the cached object be invalidated after that many seconds irregardless of how many times or when it was requested
		
		CacheManager.getInstance().addCacheIfAbsent(presenceCache);
	}
	
	@Override
	public void run() {
		
		logger.info("XMPP Service thread running");
		XmppSettings settings = StorageSystem.getInstance().load(XmppSettings.class, "xmpp", true);
		
		login = settings.getLogin();
		password = settings.getPassword();
		
		if (settings.getProxyUsers() != null)
			proxyUsers = settings.getProxyUsers();
		
		if(login == null || password == null) {
			logger.error("Login and Password not set");
			return;
		}
		
		try {
			connection = new XMPPConnection(new ConnectionConfiguration(
					settings.getHost(), 
					settings.getPort(),
					settings.getService()));
			
			logger.info("XMPP Service started");
			while(keepAlive) {
				if (!(connection.isConnected() && connection.isAuthenticated())) {
					this.connect();
				}
				
				sendPendingMessages();
				
				Thread.sleep(watchDog * 1000);
			}
			logger.info("Service keep-alive set to false");
		} catch(InterruptedException e) {
			logger.warn("Connection interrupted");
		} catch(Exception e) {
			logger.warn("Ops!!! Something bad happen.",e);
		} finally {
			logger.debug("Closing connection...");
			if (connection != null && connection.isConnected()) {
				connection.disconnect();
			}
		}
		logger.debug("Service Stoped");
	}
	
	private void sendPendingMessages() {
		Message toSend = getNextMessage();
		while (toSend != null) {
			if (toSend instanceof XmppFileMessage) {
				XmppFileMessage fileMessage = (XmppFileMessage)toSend;
				sendFile(fileMessage);
			} else if (toSend instanceof XmppMessage) {
				XmppMessage xmppMessage = (XmppMessage)toSend;
				
				sendTextMessage(xmppMessage.getRemoteId(), xmppMessage.toText());
			} else {
				logger.warn("Unknow message type {}", toSend.getClass());
			}
			
			toSend = getNextMessage();
		}
	}
	
	private void sendTextMessage(String destination, StringBuilder message) {
		
		if (!destination.equals(login)) {
			logger.debug("Sending \"{}...\" message to {}", message.substring(0, Math.min(15, message.length())), destination);
			org.jivesoftware.smack.packet.Message.Type type = org.jivesoftware.smack.packet.Message.Type.chat;
			org.jivesoftware.smack.packet.Message msg = new org.jivesoftware.smack.packet.Message(destination, type);
			msg.setBody(message.toString());
			connection.sendPacket(msg);
		}
	}
	
	private boolean sendFile(XmppFileMessage fileMessage) {
		try {
			FileTransferManager manager = new FileTransferManager(connection);
			OutgoingFileTransfer transfer = manager.createOutgoingFileTransfer(fileMessage.getRemoteId());
			transfer.sendFile(fileMessage.getFile(), fileMessage.toString());
			
			for (int c = 0; c < 10 && !transfer.isDone(); c++) {
				logger.info("Tranfer status: {} / {} bytes", transfer.getStatus(), transfer.getBytesSent());
				Thread.sleep(1000);
			}
			return true;
		} catch (XMPPException e) {
			logger.error("Error sending file {}", fileMessage.getFile().getName(),e);
		} catch (InterruptedException e) {
			logger.error("Error sending file {}", fileMessage.getFile().getName(),e);
		}
		return false;
	}
	
	private boolean connect() {
		try {
			connection.disconnect();
		} catch (IllegalStateException e) {
			logger.warn("Unecessary disconnection",e);
		}
		try {
			connection.connect();
			connection.addPacketListener(this, null);
			connection.login(login, password);
			connection.getRoster().setSubscriptionMode(SubscriptionMode.manual);
			connection.sendPacket(new Presence(Type.available, "Spike", 0, Mode.available));
			logger.info("Connected");
			
			FileTransferNegotiator.setServiceEnabled(connection, true);
			manager = new FileTransferManager(connection);
			manager.addFileTransferListener(this);
			return true;
		} catch (XMPPException e) {
			logger.warn("Connection failed",e);
		} catch (IllegalStateException e) {
			logger.warn("Unecessary connection",e);
		}
		return false;
	}
	
	@Override
	public boolean subscribe(String remoteId) {
		try {
			blockedUsers.remove(remoteId);
			{
				Presence packet = new Presence(Presence.Type.available);
				packet.setTo(remoteId);
				packet.setFrom(login);
				connection.sendPacket(packet);
			}
			return true;
		} catch (Exception e) {
			logger.warn("Subscribe packet failed",e);
		}
		return false;
	}
	
	@Override
	public boolean unsubscribe(String remoteId) {
		try {
			if (proxyUsers.containsKey(remoteId)) {
				return true;
			}
			
			blockedUsers.add(remoteId);
			{
				Presence packet = new Presence(Presence.Type.unsubscribe);
				packet.setTo(remoteId);
				packet.setFrom(login);
				connection.sendPacket(packet);
			}
			{
				Presence packet = new Presence(Presence.Type.unavailable);
				packet.setTo(remoteId);
				packet.setFrom(login);
				connection.sendPacket(packet);
			}
			return true;
		} catch (Exception e) {
			logger.warn("Subscribe packet failed",e);
		}
		return false;
	}
	
	public void reply(Message message, CharSequence alert) {
		if (message instanceof ImMessage) {
			String from = ((ImMessage)message).getRemoteId();
			String proxy = null;
			
			if (message instanceof XmppMessage)
				proxy = ((XmppMessage)message).getProxyId();
			
			sendTextMessage(proxy == null ? from : proxy, new StringBuilder(alert));
		} else {
			logger.warn("Can not reply message type: {}", message.getClass());
		}
	}
	
	@Override
	public Message createMessage(StringBuilder message, Object... args) {
		return new XmppMessage((String)args[0], message);
	}
	
	@Override
	public void reply(Message message, DefaultMsgs msg) {
		reply(message, msg.toString());
	}
	
	@Override
	public void processPacket(Packet packet) {
		String from = packet.getFrom().split("/")[0];
		String proxyId = null;
		
		if (from.equals(login)) {
			return;
		}
		
		if (proxyUsers.containsKey(from)) {
			proxyId = from;
			from = proxyUsers.get(from);
		}
		if (blockedUsers.contains(from)) {
			logger.warn("Blocked user message: {}", from);
			return;
		}
		
		if (packet instanceof Presence) {
			if (proxyId == null) {
				processPresenceUpdate((Presence)packet, from);
			}
			return;
		}
		
		if (packet instanceof org.jivesoftware.smack.packet.Message) {
			addIncome(new XmppMessage(from, proxyId, ((org.jivesoftware.smack.packet.Message)packet).getBody() ));
			return;
		}
		
		logger.warn("Unknown packet type ({}) from: {}", packet.getClass(), packet.getFrom());
		if (packet.getError() != null) {
			logger.warn("Packet error message: {}",packet.getError());
		}
	}
	
	private void processPresenceUpdate(Presence presence, String from) {
		
		//Check on cache
		Element element = presenceCache.get(from);
		if (element != null && !element.isExpired()) {
			Presence last = (Presence) element.getObjectValue();
			if (last != null && last.getType() == presence.getType() && last.getMode() == presence.getMode()) {
				logger.trace("Already known presence status {} / {} from {}", new Object[]{presence.getType(), presence.getMode(), from});
				return;
			}
		}
		
		presenceCache.put(new Element(from, presence));
		
		ImStatus status = null;
		switch(presence.getType()){
			case available:
				if (presence.getMode() == null)
					status = ImStatus.ONLINE_AVAILABLE;
				else {
					switch (presence.getMode()) {
					case away:
					case xa:
						status = ImStatus.ONLINE_AWAY;
						break;
					case dnd:
						status = ImStatus.ONLINE_BUSY;
						break;
					default:
						status = ImStatus.ONLINE_AVAILABLE;
						break;
					}
				}
				
				break;
			case unavailable:
				status = ImStatus.OFFLINE;
				break;
				
			case subscribe:
				status = ImStatus.SUBSCRIBE;
				break;
		
			default:
				break;
		}
		
		if (status != null) {
			addIncome(new XmppStatusMessage(from, status));
		}
	}
	
	@Override
	public void fileTransferRequest(FileTransferRequest request) {
		
		logger.info("Receiving file: {}",
				String.format("Receiving file: %1$c (%2$c) from %3$c (size: %4$d)", 
				request.getFileName(),
				request.getMimeType(),
				request.getRequestor(),
				request.getFileSize()));
		
		IncomingFileTransfer transfer = request.accept();
		File fRef = new File("resouces/" + request.getFileName());
		if (fRef.exists())
			fRef.delete();
		
		try {
			transfer.recieveFile(fRef);
		} catch (XMPPException e) {
			logger.warn("Tranfer failed",e);
		}
	}
}
